#!/usr/bin/env python

#    PermFS - A file system for privilege separation
#    Copyright (C) 2011 dionescu, usmanm
#

import os, stat, errno, sys
import datetime
import shutil
import fcntl
import gi, perm
import subprocess
# Pull in some spaghetti to make this stuff work 
# without fuse-py being installed
try:
    import _find_fuse_parts
except ImportError:
    pass
import fuse
from fuse import Fuse

fuse.fuse_python_api = (0, 2)
fuse.feature_assert('stateful_files', 'has_init')

DEBUG = False
DEBUGPATH = '/tmp/permfs_debug.out'

def log(msg):
    if DEBUG:
        f = open(DEBUGPATH, 'a')
        f.write(str(datetime.datetime.now()) + ' > ' + str(msg) + '\n') 
        f.close()


def flag2mode(flags):
    md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
    m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]

    if flags | os.O_APPEND:
        m = m.replace('w', 'a', 1)

    return m

class File:
    def __init__(self, f, fd):
        self.file = f
        self.fd = fd

    def __str__(self):
        return 'File: [' + str(self.file) + ', ' + str(self.fd) + ']'

class Stat(fuse.Stat):
    def __init__(self, path, st, perm, context, gimp):
        uid = context['uid']
        gids = gimp.get_gids(uid)

        r = perm.check_read(path, uid, gids)
        w = perm.check_write(path, uid, gids)
        x = perm.check_execute(path, uid, gids)

        self.st_mode = st.st_mode
        self.st_mode &= ~(stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # Clear permission bits
        if r: self.st_mode = self.st_mode | (stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
        if w: self.st_mode = self.st_mode | (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
        if x: self.st_mode = self.st_mode | (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

        self.st_uid = perm.get_uid(path)
        self.st_gid = perm.get_gid(path)

        self.st_ino = st.st_ino         # Ignored, but required
        self.st_dev = st.st_dev         # Ignored, but required

        # Note: Wiki says st_blksize is required (like st_dev, ignored but
        # required). However, this breaks things and another tutorial I found
        # did not have this field.

        self.st_blocks = st.st_blocks
        self.st_blksize = st.st_blksize

        self.st_nlink = st.st_nlink
        self.st_size = st.st_size

        now = datetime.datetime.utcnow()
        self.st_atime = st.st_atime or calendar.timegm(now.timetuple())
        self.st_mtime = st.st_mtime or calendar.timegm(now.timetuple())
        self.st_ctime = st.st_ctime or calendar.timegm(now.timetuple())

    def __str__(self):
        s = 'Stat[st_mode=' + str(self.st_mode)
        s += ', st_ino=' + str(self.st_ino)
        s += ', st_dev=' + str(self.st_dev)
        s += ', st_nlink=' + str(self.st_nlink)
        s += ', st_uid=' + str(self.st_uid)
        s += ', st_gid=' + str(self.st_gid)
        s += ', st_size=' + str(self.st_size)
        s += ', st_atime=' + str(self.st_atime)
        s += ', st_mtime=' + str(self.st_mtime)
        s += ', st_ctime=' + str(self.st_ctime)
        s += ']'
        return s

class PermFS(Fuse):

    def __init__(self, *args, **kw):
        log('init: PermFS')
        Fuse.__init__(self, *args, **kw)

    def fsinit(self):
        # Get mount path
        patharg = sys.argv[-1]
        if patharg[-1] == '/':
            patharg = patharg[:-1]

        # Can't mount to root
        if patharg == '':
            return -errno.EACCES

        # Set root path
        origpath = patharg
        patharg = os.path.split(patharg)
        self.rootpath = os.path.join(patharg[0], '.' + patharg[1])
        log('fsinit: rootpath set to ' + str(self.rootpath))

        self.perm = perm.Perm(self.rootpath)
        self.gimp = gi.GIDImporter()

        # Bootstrap for root dir
        if not self.perm.entry_exists('/'):
            log('fsinit: bootstrapping root dir creation')
            self.perm.add_file('/', int('777', 8), 0, 0) # Owned by root 

        if os.path.exists(self.rootpath):
            log('fsinit: doing recursive db check on ' + str(self.rootpath))
            # Do recursive check on db for perms
            if not os.path.isdir(self.rootpath) or not self.perm.entry_exists('/') or not self.checkfs('/'):
                subprocess.call(["fusermount", "-u", origpath])
    
        else:
            log('fsinit: creating rootpath ' + str(self.rootpath))
            os.makedirs(self.rootpath)
            
    def checkfs(self, path):
        log('checkfs: call on '+ str(path))
        value = True
        filelist = os.listdir(self.rootpath+path)
        for f in filelist:
            log('checkfs: f =  ' + str(f))
            if not self.perm.entry_exists(path+f):
                log('checkfs: f ' + str(f) + ' does not have DB entry')
                return False
            if os.path.isdir(self.rootpath+path+f):
                log('checkfs: subdirectory ' + str(f))
                value = value and self.checkfs(os.path.join(path, f))
        log('checkfs: the returned value for ' + str(path) + " is " + str(value))
        return value
        
    def getattr(self, path):
        log('getattr: ' + str(path))

        phypath = self.rootpath + path
        osstat = os.lstat(phypath)
        context = self.GetContext()
        log("getattr: original stat for " + str(phypath) + ": " + str(osstat))

        if self.perm.entry_exists(path):
            mystat = Stat(path, osstat, self.perm, context, self.gimp)
            log("getattr: my stat for " + str(path) + ": " + str(mystat))
            return mystat

        log("getattr: fail! path " + str(path) + " not in DB")
        
        return -errno.ENOENT
    
    def getxattr(self, path, name, size):       
        log('getxattr:' + str(path) + ' ' + str(name) + ' ' + str(size))

        if not self.perm.entry_exists(path):
            log('getxattr: invalid path ' + str(path))
            return -errno.ENOENT

        if name == 'perm-r':
            return self.perm.get_r(path)
        if name == 'perm-w':
            return self.perm.get_w(path)
        if name == 'perm-x':
            return self.perm.get_x(path)
        if name == 'perm-p':
            return self.perm.get_p(path)

    def setxattr(self, path, name, value, size):
        log('setxattr:' + str(path) + ' ' + str(name) + ' ' + str(value))

        if not self.perm.entry_exists(path):
            log('setxattr: invalid path ' + str(path))        
            return -errno.ENOENT

        uid = self.GetContext()['uid']
        gids = self.gimp.get_gids(uid)
        p = self.perm.check_perm(path, uid, gids)

        if not p:
            log('setxattr: cant change perms for ' + str(path))        
            return -errno.EACCES
            
        noerr = True

        if name == 'perm-r':
            noerr = self.perm.update_r(path, value)
        if name == 'perm-w':
            noerr = self.perm.update_w(path, value)
        if name == 'perm-x':
            noerr = self.perm.update_x(path, value)
        if name == 'perm-p':
            noerr = self.perm.update_p(path, value)

        if not noerr:
            return -errno.EINVAL

    def chmod(self, path, mode):
        log('chmod: ' + str(path) + ' ' + str(mode))

        context = self.GetContext()
        owneruid = self.perm.get(path)['uid']
        
        if context['uid'] == 0 or context['uid'] == owneruid:
            log("chmod: perm granted for " + str(context['uid']) + " on " + str(path))
            self.perm.update_mode(path,mode)
            return 0
        log("chmod: fail for uid " + str(context['uid']) + " on " + str(path))
        return -errno.EACCES

    def chown(self, path, uid, gid):
        log('chown: ' + str(path) + ' ' + str(uid) + ' ' + str(gid))

        context = self.GetContext()
        
        if context['uid'] == 0:
            log("chown: perm granted on " + str(path))
            self.perm.update_uid(path,uid)
            self.perm.update_gid(path,gid)
            return 0

        log("chown: fail for uid " + str(context['uid']) + " on " + str(path))
        return -errno.EACCES

    def readlink(self, path): # Not supported
        log('readlink: ' + str(path))
        return -errno.EOPNOTSUPP

    def link(self, target, name):
        log('link: ' + str(target) + ' ' + str(name))

        context = self.GetContext()
        parentdir = os.path.split(name)[0]
        gids = self.gimp.get_gids(context['uid'])
        w = self.perm.check_write(parentdir, context['uid'], gids)
        
        if not w:
            log('link: failed, no write perm on parent dir ' + str(parentdir))
            return -errno.EACCES

        log('link: has w perm on parent, go ahead for ' + str(name))
        tup = self.perm.get(target)

        if tup == None:
            log('link: failed, name is invalid ' + str(target))
            return -errno.EINVAL

        log('link: creating copy in db ' + str(name) + '->' + str(target))

        self.perm.add_file(name, tup['mode'], tup['uid'], tup['gid'])
        self.perm.update_r(name, tup['r'])
        self.perm.update_w(name, tup['w'])
        self.perm.update_x(name, tup['x'])
        self.perm.update_p(name, tup['p'])

        try:
            log('link: creating phy links')
            os.link(self.rootpath + target, self.rootpath + name)
        except OSError as e:
            log('link: creating phy links failed')
            self.perm.delete_file(name)
            return -e.errno

        log('link: success ' + str(name) + '->' + str(target))
        return 0
        
    def mknod(self, path, mode, dev):
        log('mknod: ' + str(path) + ' ' + str(mode) + ' ' + str(dev))

        npbits = mode & int('0770000', 8)

        if npbits & stat.S_IFREG:
            log('mknod: request for regular file')
            
            context = self.GetContext()
            parentdir = os.path.split(path)[0]
            gids = self.gimp.get_gids(context['uid'])

            # Check parent perms
            log('create: checking w perms on parent dir')
            w = self.perm.check_write(parentdir, context['uid'], gids)
            if not w:
                return -errno.ACCES
        
            log('create: adding to db file at ' + str(path))
            self.perm.add_file(path, int(mode), context['uid'], context['gid'])

            phypath = self.rootpath + path
            phymode = (mode & ~int('777', 8)) | int('700', 8) # 700 bits for us
            phymode = mode

            try:
                log('mknod: phypath ' + str(phypath) + ', phymode ' + str(phymode))
                os.mknod(phypath, phymode, dev)
                log('mknod: created phyfile at ' + str(phypath))
                return 0
            except OSError as e:
                log('mknod: failed to create phyfile at' + str(phypath))
                self.perm.delete_file(path)
                return -e.errno

        log('mknod: EOPNOTSUPP failed for ' + str(path) + ', mode: ' + str(mode))
        return -errno.EOPNOTSUPP

    def rename(self, old, new):
        log('rename: ' + str(old) + ' ' + str(new))

        try:
            context = self.GetContext()
            pold = os.path.split(old)[0]
            pnew = os.path.split(new)[0]
            gids = self.gimp.get_gids(context['uid'])
            wold = self.perm.check_write(pold, context['uid'], gids)
            wnew = self.perm.check_write(pnew, context['uid'], gids)

            if not wold or not wnew:
                logging.info("link: failed. no write perm for parent of " + str(old) + " or " + str(new))
                return -errno.EACCES

            log('rename: have perms to both parent dirs')

            pold = self.rootpath + old
            pnew = self.rootpath + new

            log('rename: renaming phy files ' + str(pold) + ' -> ' + str(pnew))

            os.rename(pold, pnew)

            tup = self.perm.get(old)

            log('rename: deleting ' + str(old) +' in db')

            self.perm.delete_file(old)

            log('rename: adding to db ' + str(new))

            self.perm.add_file(new, tup['mode'], tup['uid'], tup['gid'])
            self.perm.update_r(new, tup['r'])
            self.perm.update_w(new, tup['w'])
            self.perm.update_x(new, tup['x'])
            self.perm.update_p(new, tup['p'])

        except OSError as e:
            log('rename: failed! oserror')
            return -e.errno

        return 0

    def statfs(self): # Not sure what this does, ignore for now
        log('statfs')
        return os.statvfs(self.rootpath)

    def symlink(self, source, linkname): # Not supported
        log('symlink: ' + str(source) + ' ' + str(linkname))
        return -errno.EOPNOTSUPP

    def truncate(self,path,size):
        log('truncate: ' + str(path) + ', len: ' + str(size)) 
        f = open(self.rootpath + path, "a")
        f.truncate(size)
        f.close()

    def unlink(self, path):
        log('unlink: ' + str(path))

        context = self.GetContext()
        gids = self.gimp.get_gids(context['uid'])
        
        parentdir = os.path.split(path)[0]
        w = self.perm.check_write(parentdir, context['uid'], gids)

        if w:
            log('unlink: have w perms on parent dir of ' + str(path))
            try:
                phypath = self.rootpath + path
                log('unlink: unlinking phy file ' + str(path))
                os.unlink(phypath)
                self.perm.delete_file(path)
                return 0
            except OSError as e:
                log('unlink: failed oserror')
                return -e.errno
            
        log("unlink: failed, no write perms for parent of " + str(path))
        return -errno.EACCES

    def utime(self, path, times):
        log('utime: ' + str(path) + ' ' + str(times))
        return os.utime(self.rootpath + path, times)

    def fsdestroy(self):
        log('fsdestroy: ' + str(self.rootpath))
        self.perm.close()
        self.gimp.close()
    
    # DIR METHODS

    def opendir(self, path):
        log('opendir:' + str(path))
        
        phypath = self.rootpath + path
        
        try:
            st = os.lstat(phypath)
            if not stat.S_ISDIR(st.st_mode):
                log('opendir: error ' + str(path) + ' is not a dir')
                return -errno.ENOENT
        except OSError as e:
            log('opendir: ' + str(path) + ' doesnt exist')
            return -e.errno

        uid = self.GetContext()['uid']
        gids = self.gimp.get_gids(uid)
        r = self.perm.check_read(path, uid, gids)
        if not r:
            log('opendir: dont have r perms on ' + str(path))
            return -errno.EACCES

        log('openddir: ' + str(path) + ' success')
        return 0

    def readdir(self, path, offset):
        log('readdir: ' + str(path) + str(offset))

        yield fuse.Direntry(".")
        yield fuse.Direntry("..")
        for e in os.listdir(self.rootpath + path):
            yield fuse.Direntry(e)

    def mkdir(self, path, mode):
        log('mkdir: ' + str(path) + ' ' + str(mode))

        context = self.GetContext()
        uid = context['uid']
        gids = self.gimp.get_gids(uid)
        parentdir = os.path.split(path)[0]

        # Check w perms on parent dir
        log('mkdir: checking w perms on parent dir ' + str(parentdir))
        w = self.perm.check_write(parentdir, uid, gids)
        if not w:
            log('mkdir: fail! no w perms on parent dir ' + str(parentdir))
            return -errno.EACCES

        # Add file to db
        log('mkdir: adding to db ' + str(path))
        self.perm.add_file(path, mode, uid, context['gid'])

        phypath = self.rootpath + path
        phymode = (mode & ~int('777', 8)) | int('700', 8)

        try:
            log('mkdir: creating phy dir ' + str(phypath))
            os.mkdir(phypath, phymode)
        except OSError as e:
            log('mkdir: delete db entry, failed to create phy dir ' + str(phypath))
            self.perm.delete_file(path)
            return -e.errno

        return 0

    def rmdir(self, path):
        log('rmdir: ' + str(path))

        context = self.GetContext()
        uid = context['uid']
        gids = self.gimp.get_gids(uid)
        
        parentdir = os.path.split(path)[0]
        w = self.perm.check_write(parentdir, uid, gids)
        
        if w:
            log("rmdir: perm to remove " + str(path) +" granted")
            try:
                phypath = self.rootpath + path
                os.rmdir(phypath)
                self.perm.delete_file(path)
                return 0
            except OSError as e:
                log("rmdir: oseror while removing dir")
                return -e.errno
        log("rmdir: no w perm for "+str(path))
        return -errno.EACCES

    # FILE METHODS

    def open(self, path, flags):
        log('open: ' + str(path))

        uid = self.GetContext()['uid']
        gids = self.gimp.get_gids(uid)

        try:
            flmode = flag2mode(flags)
            log('open: flmode is ' + str(flmode))

            if 'w' in flmode or 'a' in flmode:
                log('open: checking w perms...')
                w = self.perm.check_write(path, uid, gids)
                if not w:
                    raise OSError(-errno.EACCES)

            if 'r' in flmode:
                log('open: checking r perms...')
                r = self.perm.check_read(path, uid, gids)
                if not r:
                    raise OSError(-errno.EACCES)

            log('open: perms granted, opening...')
            f = os.fdopen(os.open(self.rootpath + path, flags),
                                  flag2mode(flags))
            fd = f.fileno()
            fl = File(f, fd)
            log('f: ' + str(f) + ', fd: ' + str(fd) + ', file: ' + str(fl))
            return fl
        except OSError as e:
            log('open: failed to open file at ' + str(path))
            return -e.errno

    # create = mknod + open
    
    def fgetattr(self, path, fh=None):
        log('fgetattr: ' + str(path))

        st = os.fstat(fh.fd)
        log('fgetattr: os.fstat is ' + str(st))
        st = Stat(path, st, self.perm, self.GetContext(), self.gimp)
        log('fgetattr: generated stat is ' + str(st))
        return st
        
    def release(self, path, flags, fh=None):
        log('release: ' + str(path))
        fh.file.close()

    def fsync(self, path, datasync, fh=None):
        log('fsync: ' + str(path))

        if 'w' in fh.file.mode or 'a' in fh.file.mode:
            fh.file.flush()

        if datasync and hasattr(os, 'fdatasync'):
            os.fdatasync(fh.fd)
        else:
            os.fsync(fh.fd)

    def flush(self, path, fh=None):
        log('flush: ' + str(path))

        if 'w' in fh.file.mode or 'a' in fh.file.mode:
            fh.file.flush()
        os.close(os.dup(fh.fd))

    def read(self, path, size, offset, fh=None):
        log('read: ' + str(path))
        fh.file.seek(offset)
        return fh.file.read(size)

    def write(self, path, buf, offset, fh=None):
        log('write: ' + str(path) + ' buf: ' + str(buf))
        fh.file.seek(offset)
        fh.file.write(buf)
        return len(buf)

    def ftruncate(self, path, size, fh=None):
        log('ftruncate: ' + str(path))
        fh.file.truncate(size)

    def main(self, *a, **kw):
        return Fuse.main(self, *a, **kw)

def main():
    log("main: argv length: " + str(len(sys.argv)))
    for i in range(len(sys.argv)):
        log("main: arg " + str(i) + ": " + str(sys.argv[i]))

    usage="""
Userspace PermFS

""" + Fuse.fusage
    server = PermFS(version="%prog " + fuse.__version__,
                     usage=usage,
                     dash_s_do='setsingle')

    server.parse(errex=1)
    server.main()
    
if __name__ == '__main__':
    main()
